#include <stdint.h>
#include <sunxispi.h>
#include <linux/kernel.h>

#define SPI0_BASE_ADDR 0x1c68000

#define SUNXI_SPI_FIFO_RF_CNT_MASK 0x7f
#define SUNXI_SPI_FIFO_RF_CNT_BITS 0
#define SUNXI_SPI_CTL_XCH BIT(31)

#define SUNXI_SPI_BURST_CNT(cnt) ((cnt) & 0xffffff)
#define SUNXI_SPI_XMIT_CNT(cnt) ((cnt) & 0xffffff)

#define SUNXI_SPI_CTL_CS_MASK 0x30
#define SUNXI_SPI_CTL_CS_LEVEL BIT(7)
#define SUNXI_SPI_CTL_CS(cs) (((cs) << 4) & SUNXI_SPI_CTL_CS_MASK)

#define SUNXI_SPI_CTL_ENABLE BIT(0)
#define SUNXI_SPI_CTL_MASTER BIT(1)
#define SUNXI_SPI_CTL_TP BIT(7)
#define SUNXI_SPI_CTL_SRST BIT(31)

#define SUNXI_SPI_CTL_CS_MANUAL BIT(6)
#define SUNXI_SPI_CTL_RF_RST BIT(15)
#define SUNXI_SPI_CTL_TF_RST BIT(31)

struct sunxi_spi_regs
{
	uint32_t unused0[1];
	uint32_t glb_ctl;	/* 0x04 */
	uint32_t xfer_ctl;	/* 0x08 */
	uint32_t unused1[1];
	uint32_t int_ctl;	/* 0x10 */
	uint32_t int_sta;	/* 0x14 */
	uint32_t fifo_ctl;	/* 0x18 */
	uint32_t fifo_sta;	/* 0x1c */
	uint32_t wait;		/* 0x20 */
	uint32_t clk_ctl;	/* 0x24 */
	uint32_t unused2[2];
	uint32_t burst_cnt;	/* 0x30 */
	uint32_t xmit_cnt;	/* 0x34 */
	uint32_t burst_ctl;	/* 0x38 */
	uint32_t unused3[113];
	uint32_t tx_data;	/* 0x200 */
	uint32_t unused4[63];
	uint32_t rx_data;	/* 0x300 */
};

struct sunxi_spi_bus
{
    struct SpiBus parent;
    uint32_t base;
};

struct sunxi_spi_dev
{
    struct SpiDev parent;
    int cs;
};

static struct sunxi_spi_bus spi0;

static struct sunxi_spi_dev spi0dev0;

static void sunxi_spi_reset(struct sunxi_spi_bus *sunxi_spi_bus)
{
	struct sunxi_spi_regs *regs = (struct sunxi_spi_regs*)sunxi_spi_bus->base;
	uint32_t reg;
	
	reg = readl(&regs->glb_ctl);
	reg |= (SUNXI_SPI_CTL_MASTER | SUNXI_SPI_CTL_ENABLE | SUNXI_SPI_CTL_TP | SUNXI_SPI_CTL_SRST);
	writel(reg, &regs->glb_ctl);
	
	while (readl(&regs->glb_ctl) & SUNXI_SPI_CTL_SRST);
	
	reg = readl(&regs->xfer_ctl);
	reg |= (SUNXI_SPI_CTL_CS_MANUAL | SUNXI_SPI_CTL_CS_LEVEL);
	writel(reg, &regs->xfer_ctl);
	
	reg = readl(&regs->fifo_ctl);
	reg |= (SUNXI_SPI_CTL_RF_RST | SUNXI_SPI_CTL_TF_RST);
	writel(reg, &regs->fifo_ctl);
}

static void sunxi_spi_configure(struct SpiBus *spi_bus, const struct SpiConfig *config)
{
    PRINT_WARN("TODO: %s\n", __func__);
}

static void sunxispi_hostwrite(struct SpiBus *spi_bus, const char *tx_buf,
	size_t nbytes)
{
    struct sunxi_spi_bus *sunxi_spi_bus = (struct sunxi_spi_bus*)spi_bus;
	struct sunxi_spi_regs *regs = (struct sunxi_spi_regs*)sunxi_spi_bus->base;
	size_t i;
	char byte;
	
	if (!tx_buf)
		nbytes = 0;

	writel(SUNXI_SPI_XMIT_CNT(nbytes), &regs->xmit_cnt);
	writel(SUNXI_SPI_BURST_CNT(nbytes), &regs->burst_ctl);

	for (i = 0; i < nbytes; ++i) {
		byte = tx_buf ? *tx_buf++ : 0;
		writeb(byte, &regs->tx_data);
	}
}

static void sunxi_spi_xfer(struct SpiBus *spi_bus, const void *send, void *recv, size_t len)
{
    struct sunxi_spi_bus *sunxi_spi_bus = (struct sunxi_spi_bus*)spi_bus;
	struct sunxi_spi_regs *regs = (struct sunxi_spi_regs*)sunxi_spi_bus->base;

	uint32_t reg;
	const char *tx_buf = send;
	char *rx_buf = recv;
	size_t i, nbytes;
	char byte;

	while (len) {
		nbytes = min(len, (size_t)64 - 1);

		writel(SUNXI_SPI_BURST_CNT(nbytes), &regs->burst_cnt);
		sunxispi_hostwrite(spi_bus, tx_buf, nbytes);
		reg = readl(&regs->xfer_ctl);	
		reg |= SUNXI_SPI_CTL_XCH;
		writel(reg, &regs->xfer_ctl);
		
		while (((readl(&regs->fifo_sta) &
			SUNXI_SPI_FIFO_RF_CNT_MASK) >>
			SUNXI_SPI_FIFO_RF_CNT_BITS) < nbytes);

		for (i = 0; i < nbytes; ++i) {
			byte = readb(&regs->rx_data);

			if (rx_buf)
				*rx_buf++ = byte;
		}

		len -= nbytes;

		if (tx_buf)
			tx_buf += nbytes;
	}
}

static void sunxi_spi_take(struct SpiDev *spi_dev)
{
    struct sunxi_spi_dev *sunxi_spi_dev = (struct sunxi_spi_dev*)spi_dev;
    struct sunxi_spi_bus *sunxi_spi_bus = (struct sunxi_spi_bus*)spi_dev->bus;
	struct sunxi_spi_regs *regs = (struct sunxi_spi_regs*)sunxi_spi_bus->base;
	uint32_t reg;

	reg = readl(&regs->xfer_ctl);
	reg &= ~(SUNXI_SPI_CTL_CS_MASK | SUNXI_SPI_CTL_CS_LEVEL);
	reg |= SUNXI_SPI_CTL_CS(sunxi_spi_dev->cs);
	writel(reg, &regs->xfer_ctl);

}

static void sunxi_spi_release(struct SpiDev *spi_dev)
{
    struct sunxi_spi_bus *sunxi_spi_bus = (struct sunxi_spi_bus*)spi_dev->bus;
	struct sunxi_spi_regs *regs = (struct sunxi_spi_regs*)sunxi_spi_bus->base;
	uint32_t reg;

	reg = readl(&regs->xfer_ctl);
	reg &= ~SUNXI_SPI_CTL_CS_MASK;
	reg |= SUNXI_SPI_CTL_CS_LEVEL;
	writel(reg, &regs->xfer_ctl);
}

static void sunxi_spi_dev_init(struct sunxi_spi_bus *bus, struct sunxi_spi_dev *dev, int cs)
{
    SpiDevInit(&dev->parent);
    dev->parent.bus = &bus->parent;
    dev->cs = cs;
}

static void sunxi_spi_bus_init(struct sunxi_spi_bus *bus, uint32_t base)
{
    SpiBusInit(&bus->parent);
    bus->base = IO_DEVICE_ADDR(base);
    bus->parent.configure = sunxi_spi_configure;
    bus->parent.take = sunxi_spi_take;
    bus->parent.release = sunxi_spi_release;
    bus->parent.xfer = sunxi_spi_xfer;
    sunxi_spi_reset(bus);
}

struct SpiBus *sunxi_spi_bus_get(int bus)
{
    if (bus)
        return NULL;
    return &spi0.parent;
}
struct SpiDev *sunxi_spi_dev_get(int bus, int dev)
{
    if (bus || dev)
        return NULL;
    return &spi0dev0.parent;
}

void sunxi_spi_init()
{
    sunxi_spi_bus_init(&spi0, SPI0_BASE_ADDR);
    sunxi_spi_dev_init(&spi0, &spi0dev0, 0);
}

